package com.shycloud.mido.member.service.impl;

import com.shycloud.mido.member.api.vo.TeacherCourseVO;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.TimeUnit;

import org.springframework.beans.BeanUtils;
import org.springframework.beans.BeansException;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.shycloud.mido.common.api.constants.OnlineCourseConstants;
import com.shycloud.mido.common.api.enums.ScheduleRemindTypeEnum;
import com.shycloud.mido.common.core.constant.CommonConstants;
import com.shycloud.mido.common.core.constant.enums.OnlineCourseEnum;
import com.shycloud.mido.common.core.exception.BusinessException;
import com.shycloud.mido.common.core.util.R;
import com.shycloud.mido.common.security.util.SecurityUtils;
import com.shycloud.mido.common.util.DateUtil;
import com.shycloud.mido.common.util.rtc.RTCUtil;
import com.shycloud.mido.member.api.entity.BasicGoodsInfo;
import com.shycloud.mido.member.api.entity.Member;
import com.shycloud.mido.member.api.entity.OnlineCourseInfo;
import com.shycloud.mido.member.api.entity.OnlineCoursePeriod;
import com.shycloud.mido.member.api.entity.OnlineCourseRecord;
import com.shycloud.mido.member.api.entity.OnlineCourseSchedule;
import com.shycloud.mido.member.api.entity.OrgOrganization;
import com.shycloud.mido.member.api.entity.OrgStudentDetailed;
import com.shycloud.mido.member.api.entity.OrgTeacherDetailed;
import com.shycloud.mido.member.api.vo.AccessRoomReturnVO;
import com.shycloud.mido.member.api.vo.AppPayOnlineCourseVO;
import com.shycloud.mido.member.api.vo.BeatDataDTO;
import com.shycloud.mido.member.api.vo.HeartBeatReturnVO;
import com.shycloud.mido.member.api.vo.OnlineCoursePeriodVO;
import com.shycloud.mido.member.api.vo.OnlineCourseRecordVO;
import com.shycloud.mido.member.api.vo.OnlineCourseRequestVO;
import com.shycloud.mido.member.api.vo.OnlineCourseScheduleVO;
import com.shycloud.mido.member.api.vo.OnlineCourseTrialInfoReturnVO;
import com.shycloud.mido.member.api.vo.RoomInfo;
import com.shycloud.mido.member.api.vo.TrialRoomInfo;
import com.shycloud.mido.member.config.PropertiesConfig;
import com.shycloud.mido.member.mapper.BasicGoodsInfoMapper;
import com.shycloud.mido.member.mapper.MemberMapper;
import com.shycloud.mido.member.mapper.OnlineCourseInfoMapper;
import com.shycloud.mido.member.mapper.OnlineCoursePeriodMapper;
import com.shycloud.mido.member.mapper.OnlineCourseRecordMapper;
import com.shycloud.mido.member.mapper.OnlineCourseScheduleMapper;
import com.shycloud.mido.member.mapper.OrgOrganizationMapper;
import com.shycloud.mido.member.mapper.OrgStudentDetailedMapper;
import com.shycloud.mido.member.mapper.OrgTeacherDetailedMapper;
import com.shycloud.mido.member.mapper.StockQuantityMapper;
import com.shycloud.mido.member.mapper.TeacherStudentMapper;
import com.shycloud.mido.member.service.OnlineCourseService;
import com.shycloud.mido.opter.api.entity.StockQuantity;
import com.shycloud.mido.organ.api.entity.TeacherStudentEntity;

import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;

/**
 * @author Xiaoxing Yu
 * @date 2020/10/15
 **/
@Slf4j
@Service
@AllArgsConstructor
public class OnlineCourseServiceImpl implements OnlineCourseService {

	private OnlineCourseRecordMapper recordMapper;

	private MemberMapper memberMapper;

	private OrgOrganizationMapper organMapper;

	private OrgTeacherDetailedMapper orgTeacherMapper;

	private OrgStudentDetailedMapper orgStudentMapper;

	private TeacherStudentMapper teacherStudentMapper;

	private BasicGoodsInfoMapper goodsInfoMapper;

	private StockQuantityMapper stockQuantityMapper;

	private OnlineCoursePeriodMapper periodMapper;

	private OnlineCourseInfoMapper infoMapper;

	private OnlineCourseScheduleMapper scheduleMapper;

	private RedisTemplate redisTemplate;

	private PropertiesConfig propertiesConfig;

	@Override
	@Transactional(rollbackFor = Exception.class)
	public R accessRoom(String memberId, OnlineCourseRequestVO requestVO) {
		AccessRoomReturnVO returnVO = null;
		String concatKey = null;
		String studentId = null;
		String teacherId = null;
		//type 0-老师 1-学生
		Integer type = null;
		Boolean notAccessed = null;
		RoomInfo roomInfo = null;
		TrialRoomInfo trialRoomInfo = null;
		//是否是体验课
		Boolean isTrial = requestVO.getIsTrial();
		String studentKey = null;
		String teacherKey = null;

		try {
			//体验在线课模式
			if (isTrial) {
				type = requestVO.getType();
				returnVO = new AccessRoomReturnVO();
				String roomId = getTrialRoomId(requestVO.getTrialRoomShowId());
				concatKey = roomId + ":" + (type == 0 ? "t_" : "s_") + memberId;
				studentKey = roomId + ":" + "student";
				teacherKey = roomId + ":" + "teacher";

				retrieveTokenAndRoomInfo(
						trialRoomInfo, roomId, type == 0 ? memberId : null, type == 1 ? memberId : null, returnVO);

				//判断用户是否已进入房间
				notAccessed = redisTemplate.opsForValue().setIfAbsent(
						concatKey, System.currentTimeMillis(), 86400, TimeUnit.SECONDS);

				if (!notAccessed) {
					RoomInfo info = (RoomInfo) redisTemplate.opsForValue().get(roomId + ":info");
					returnVO.setFailed("进入房间失败！用户已经在房间里!");
					return new R(returnVO);
				}

				redisTemplate.opsForValue().set(type == 0 ? teacherKey : studentKey
						, memberId, 86400, TimeUnit.SECONDS);

				returnVO.setSuccess("成功进入房间", type, memberId);

				return new R(returnVO);
			} else {
				studentId = requestVO.getStudentId();
				teacherId = requestVO.getTeacherId();
				type = requestVO.getType();

				if (StringUtils.isEmpty(studentId) || StringUtils.isEmpty(teacherId)) {
					return new R(OnlineCourseConstants.ID_CANNOT_BE_EMPTY, (studentId == null ? "学生" : "老师") + "ID不能为空", false);
				}

				//验证师生关系, 通过学生去寻找与老师的对应关系
//				TeacherStudentEntity mapping = teacherStudentMapper.selectOne(
//						new QueryWrapper<TeacherStudentEntity>().eq("student_id", studentId));
//				if (mapping == null || !mapping.getTeacherId().equals(teacherId)) {
//					return new R(OnlineCourseConstants.STUDENT_TEACHER_UNMATCHED, "学生和老师无绑定关系!", false);
//				}

				//如果是老师的话，检测机构可用课时
				if (type == 0) {
					OrgTeacherDetailed orgTeacher = orgTeacherMapper.selectOne(
							new QueryWrapper<OrgTeacherDetailed>().eq("id", teacherId));
					if (orgTeacher == null) {
						return new R(OnlineCourseConstants.TEACHER_NO_ORG,
								"未找到老师信息!", false);
					}

					OrgOrganization org = organMapper.selectOne(
							new QueryWrapper<OrgOrganization>().eq("id", orgTeacher.getOrgId()));

					if (org == null) {
						return new R(OnlineCourseConstants.TEACHER_NO_ORG,
								"老师无归属机构或工作室，无法进入房间!", false);
					}
					Integer orgAvailTime = org.getOnlineCourseAvail();

					if (orgAvailTime < 0) {
						//可用时间为负，禁止老师加入房间
						return new R(OnlineCourseConstants.ORG_NO_AVAIL_TIME,
								"机构剩余课时时间(" + orgAvailTime + ")不足，请在机构后台充值剩余课时时间,谢谢!", false);
					}
				}

				String roomId = getRoomId(type, studentId, teacherId);
				concatKey = roomId + ":" + (type == 0 ? "t_" + teacherId : "s_" + studentId);
				returnVO = new AccessRoomReturnVO();

				//获取token和channel信息，如果对方已为房间生成token就不用生成
				retrieveTokenAndRoomInfo(roomInfo, roomId, teacherId, studentId, returnVO);

				//判断用户是否已进入房间
				notAccessed = redisTemplate.opsForValue().setIfAbsent(
						concatKey, System.currentTimeMillis(), 86400, TimeUnit.SECONDS);

				if (!notAccessed) {
					RoomInfo info = (RoomInfo) redisTemplate.opsForValue().get(roomId + ":info");
					returnVO.setFailed("进入房间失败！用户已经在房间里!");
					return new R(returnVO);
				}

				returnVO.setSuccess("成功进入房间", type, type == 0 ? teacherId : studentId);
				return new R(returnVO);
			}
		} catch (Exception e) {
			log.error(e.getMessage(), e);
			returnVO.setFailed("用户进入房间失败");
			redisTemplate.delete(concatKey);
			return new R(returnVO);
		}
	}

	private void retrieveTokenAndRoomInfo(RoomInfo roomInfo, String roomId,
			String teacherId, String studentId, AccessRoomReturnVO returnVO) {
		if (redisTemplate.hasKey(roomId + ":info")) {
			roomInfo = (RoomInfo) redisTemplate.opsForValue().get(roomId + ":info");
		} else {
			roomInfo = generateRTCTokenAndRoomInfo();
			roomInfo.setUsers(teacherId, studentId);

			//value设为包含accessRTCToken的RoomInfo
			redisTemplate.opsForValue().set(
					roomId + ":info", roomInfo, 86400, TimeUnit.SECONDS);
		}

		returnVO.setData(roomInfo.getToken(), roomInfo.getChannel());
	}

	private void retrieveTokenAndRoomInfo(TrialRoomInfo trialRoomInfo, String roomId,
			String teacherMemberId, String studentMemberId, AccessRoomReturnVO returnVO) {
		if (redisTemplate.hasKey(roomId + ":info")) {
			trialRoomInfo = (TrialRoomInfo) redisTemplate.opsForValue().get(roomId + ":info");
		} else {
			trialRoomInfo = generateTrialRTCTokenAndRoomInfo();
			trialRoomInfo.setUsers(teacherMemberId, studentMemberId);

			//value设为包含accessRTCToken的RoomInfo
			redisTemplate.opsForValue().set(
					roomId + ":info", trialRoomInfo, 86400, TimeUnit.SECONDS);
		}

		returnVO.setData(trialRoomInfo.getToken(), trialRoomInfo.getChannel());
	}

	@Override
	@Transactional(rollbackFor = Exception.class)
	public R exitRoom(String memberId, OnlineCourseRequestVO requestVO) {
		String studentId = null;
		String teacherId = null;
		Integer type = null;
		String logPrefix = null;
		Boolean isTrial = requestVO.getIsTrial();
		//体验用
		String studentKey = null;
		String teacherKey = null;

		try {
			type = requestVO.getType();

			if (isTrial && requestVO.getTrialRoomShowId() == null) {
				return new R(CommonConstants.FAIL, "房间ID参数不能为空！", false);
			}

			//体验房间号，也是老师手机号
			String trialRoomShowId = requestVO.getTrialRoomShowId();
			String trialRoomId = getTrialRoomId(trialRoomShowId);
			studentKey = trialRoomId + ":" + "student";
			teacherKey = trialRoomId + ":" + "teacher";
			studentId = isTrial ? (type == 1 ? memberId : (String) redisTemplate.opsForValue().get(studentKey))
					: requestVO.getStudentId();
			teacherId = isTrial ? (type == 0 ? memberId
					: memberMapper.selectOne(new QueryWrapper<Member>().eq("phone", trialRoomShowId)).getId())
					: requestVO.getTeacherId();
			logPrefix = type == 1 ? "学生(" + studentId + ")" : "老师(" + teacherId + ")";
			String roomId = isTrial ? getTrialRoomId(trialRoomShowId) : getRoomId(type, studentId, teacherId);
			String concatKey = roomId + ":" + (isTrial ? ((type == 0 ? "t_" : "s_") + memberId)
					: (type == 0 ? "t_" + teacherId : "s_" + studentId));

			if (!redisTemplate.hasKey(concatKey)) {
				return new R(OnlineCourseConstants.USER_NOT_IN_ROOM, "用户不在房间", false);
			}

			String todayStr = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
			Long startTime = (Long) redisTemplate.opsForValue().get(roomId + ":" + "startTime");
			Boolean shortDeleted = (Boolean) redisTemplate.opsForValue().get(roomId + ":" + "shortDeleted");

			if (startTime != null) {
				if (DateUtil.getTimestampMinDiffFloor(System.currentTimeMillis(), startTime) == 0 && shortDeleted != null
						&& !shortDeleted) {
					//要防止另外一端多删除记录, 此处应做处理
					Boolean unlocked = redisTemplate.opsForValue().setIfAbsent(
							roomId + ":" + "deleteLock", System.currentTimeMillis(), 5, TimeUnit.SECONDS);

					//加入分布式锁
					if (unlocked) {
						periodMapper.deleteMostRecentRecordByUsers(todayStr, studentId, teacherId);
						redisTemplate.opsForValue().set(roomId + ":" + "shortDeleted", true, 300, TimeUnit.SECONDS);
					}
				} else {
					periodMapper.updateMostRecentEndTimeByDateAndUsers(LocalDateTime.now(), todayStr, studentId, teacherId);
				}
			}

			//删除学生/老师在房间的Key
			if (!redisTemplate.delete(concatKey)) {
				throw new BusinessException(logPrefix + "离开异常!'");
			}

			if (isTrial && type == 0) {
				if (requestVO.getConsumed() == null) {
					throw new BusinessException(logPrefix + "未传入消耗时间参数!");
				}
				consumeTrialTime(memberId, requestVO.getConsumed());
			}
		} catch (BusinessException e) {
			log.error(e.getMessage(), e);
			throw new BusinessException(e.getMessage());
		} catch (Exception e) {
			log.error(e.getMessage(), e);
			return new R(OnlineCourseConstants.SERVER_ERROR, logPrefix + "房间离开时发生异常", false);
		}
		return new R(OnlineCourseConstants.SUCCESS,
				logPrefix + "房间离开成功", true);
	}

	//平均每分钟左右发送一次心跳
	//如果都收到教师和学生客户端发来的请求，则消耗在线课时长
	@Override
	@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
	public R updateHeartbeat(String memberId, OnlineCourseRequestVO requestVO) {
		BeatDataDTO beatDTO = new BeatDataDTO(0, 0);
		Integer beatIdx = null;
		String studentId = null;
		String teacherId = null;
		//type 0-老师 1-学生
		Integer type = null;
		//是否是体验课
		Boolean isTrial = requestVO.getIsTrial();
		String studentKey = null;
		String teacherKey = null;

		try {
			if (isTrial && requestVO.getTrialRoomShowId() == null) {
				return new R(CommonConstants.FAIL, "房间ID参数不能为空！", false);
			}

			String trialRoomShowId = requestVO.getTrialRoomShowId();
			String trialRoomId = getTrialRoomId(trialRoomShowId);
			studentKey = trialRoomId + ":" + "student";
			teacherKey = trialRoomId + ":" + "teacher";
			type = requestVO.getType();

			studentId = isTrial ? (type == 1 ? memberId : (String) redisTemplate.opsForValue().get(studentKey))
					: requestVO.getStudentId();
			teacherId = isTrial ? (type == 0 ? memberId : (String) redisTemplate.opsForValue().get(teacherKey))
					: requestVO.getTeacherId();
			beatIdx = requestVO.getBeatIndex();
			type = requestVO.getType();
			String roomId = isTrial ? getTrialRoomId(requestVO.getTrialRoomShowId()) : getRoomId(type, studentId, teacherId);

			//检查学生老师是否都进入教室
			if (!redisTemplate.hasKey(roomId + ":s_" + studentId) || !redisTemplate.hasKey(roomId + ":t_" + teacherId)) {
				return new R(CommonConstants.SUCCESS, type == 0 ? "老师" + "未进入教室！" : "学生" + "未进入教室！", false);
			}

			String teaPrefix = roomId + ":t_";
			String stuPrefix = roomId + ":s_";
			String teaLastBeatKey = teaPrefix + "_last_beat";
			String stuLastBeatKey = stuPrefix + "_last_beat";
			String teaCurBeatKey = teaPrefix + "_cur_beat";
			String stuCurBeatKey = stuPrefix + "_cur_beat";
			String stuTmpBeatKey = stuPrefix + "_tmp_beat";
			String teaTmpBeatKey = stuPrefix + "_tmp_beat";
			//上一次老师端记录的心跳时间戳
			Long teaLastCheckPoint = (Long) redisTemplate.opsForValue().get(teaLastBeatKey);
			//上一次学生端记录的心跳时间戳
			Long stuLastCheckPoint = (Long) redisTemplate.opsForValue().get(stuLastBeatKey);
			long curTime = System.currentTimeMillis();
			Long curCheckPoint = curTime;
			Long prevStartTime = (Long) redisTemplate.opsForValue().get(roomId + ":" + "startTime");

			//处理双方都建立连接之后的第一个心跳，判断是否两人都已进入房间，若都进入，则开始计算在线课时长
			R firstBeatProcessResult = processFirstBeat(type, roomId, studentId, teacherId,
					beatIdx, curTime, stuLastBeatKey, teaLastBeatKey, teaLastCheckPoint, stuLastCheckPoint, prevStartTime);
			if (firstBeatProcessResult != null) {
				return firstBeatProcessResult;
			}

			//计算上次和本次心跳差值
			R getDiffResult = calculateDiff(type, beatIdx, teacherId, studentId, teaTmpBeatKey, teaLastCheckPoint,
					teaLastBeatKey, curTime, curCheckPoint, stuCurBeatKey, teaCurBeatKey,
					stuTmpBeatKey, stuLastCheckPoint, stuLastBeatKey, beatDTO, isTrial);
			if (getDiffResult != null) {
				return getDiffResult;
			}

			//学生和老师双方都需要满足和上一次心跳时长大约为1分钟的时候，我们才计时在线课消耗一分钟
			if (beatDTO.getStuMinDiff() == 1 && beatDTO.getTeaMinDiff() == 1) {
				if (!isTrial) {
					//消耗一分钟在线课课时
					return consumeTime(type, beatDTO, memberId, teacherId, studentId, beatIdx);
				}
			} else if (beatDTO.getStuMinDiff() > 20 || beatDTO.getTeaMinDiff() > 20) {
				return processTimeout(beatIdx, stuLastCheckPoint, teaLastCheckPoint, beatDTO, teacherId, studentId);
			}
		} catch (BusinessException e) {
			log.error(e.getMessage(), e);
			return new R(CommonConstants.FAIL, e.getMessage(),
					new HeartBeatReturnVO(beatIdx, false, teacherId, studentId, e.getMessage()));
		} catch (Exception e) {
			log.error(e.getMessage(), e);
			return new R(CommonConstants.FAIL, e.getMessage(),
					new HeartBeatReturnVO(beatIdx, false, teacherId, studentId, e.getMessage()));
		}

		return new R(CommonConstants.SUCCESS, "扣除时间失败!", new HeartBeatReturnVO(beatIdx, true, teacherId, studentId));
	}

	@Override
	public List<OnlineCourseRecordVO> getCourseRecord(String studentId, String teacherId, Integer year, Integer month) {
		String paramMonth = month < 10 ? "0" + month : String.valueOf(month);
		return recordMapper.selectAllTeacherStudentRecordsByMonth(year, paramMonth, studentId, teacherId);
	}

	@Override
	public List<OnlineCourseRecordVO> getCourseRecordByDate(String studentId, String teacherId, String dateStr) {
		List<OnlineCourseRecordVO> recordVO =
				recordMapper.selectTeacherStudentRecordsByDate(studentId, teacherId, dateStr);
		return recordVO;
	}

	@Override
	public R addCourseRecord(String memberId, OnlineCourseRecordVO recordVO) {
		OnlineCourseRecord recordEntity = null;
		String studentId = null;
		String teacherId = null;
		String dateStr = null;

		try {
			recordEntity = new OnlineCourseRecord();
			studentId = recordVO.getStudentId();
			teacherId = recordVO.getTeacherId();
			dateStr = recordVO.getCourseDate();

			OnlineCourseInfo info = infoMapper.selectOne(
					new QueryWrapper<OnlineCourseInfo>().eq("course_date", dateStr)
							.eq("teacher_id", recordVO.getTeacherId())
							.eq("student_id", recordVO.getStudentId()));

			if (info == null) {
				return new R(OnlineCourseEnum.PERIOD_NOT_FOUND);
			}

			List<OnlineCourseRecordVO> existingRecord = recordMapper.selectTeacherStudentRecordsByDate2(studentId, teacherId, dateStr);
			if (existingRecord.size() != 0) {
				return new R(OnlineCourseEnum.COURSE_ALREADY_COMMENTED);
			}

			BeanUtils.copyProperties(recordVO, recordEntity);
			recordEntity.setCourseId(info.getId());
			recordEntity.setCommentStatus("1");
		} catch (BeansException e) {
			log.error(e.getMessage(), e);
			return new R(OnlineCourseEnum.SERVER_ERROR);
		}

		return recordMapper.insert(recordEntity) == 1 ? new R(true, OnlineCourseEnum.SUCCESS)
				: new R(OnlineCourseEnum.SERVER_ERROR);
	}

	@Override
	public Integer getAvailTime(String studentId) {
		String studentMemberId = orgStudentMapper.selectOne(
				new QueryWrapper<OrgStudentDetailed>().eq("id", studentId)).getMemberId();

		return memberMapper.selectOne(
				new QueryWrapper<Member>().eq("id", studentMemberId)).getOnlineCourseValidTime();
	}

	@Override
	public Boolean updateAvailTime(String studentId, Integer delta) {
		String studentMemberId = orgStudentMapper.selectOne(
				new QueryWrapper<OrgStudentDetailed>().eq("id", studentId)).getMemberId();

		Member member = memberMapper.selectOne(new QueryWrapper<Member>().eq("id", studentMemberId));
		if (null == member) {
			throw new BusinessException("未找到学生会员信息!");
		}

		if (member.getOnlineCourseValidTime() + delta < 0) {
			throw new BusinessException("更新后可用时间不能为负数!");
		}

		member.setOnlineCourseValidTime(member.getOnlineCourseValidTime() + delta);
		return memberMapper.updateById(member) == 1 ? true : false;
	}

	@Override
	public Boolean addAccumulcateTime(String studentId, Integer delta) {
		String studentMemberId = orgStudentMapper.selectOne(
				new QueryWrapper<OrgStudentDetailed>().eq("id", studentId)).getMemberId();

		Member member = memberMapper.selectOne(new QueryWrapper<Member>().eq("id", studentMemberId));
		if (null == member) {
			throw new BusinessException("未找到学生会员信息!");
		}
		member.setAccuOnlineCourseConsumed(member.getAccuOnlineCourseConsumed() + delta);
		return memberMapper.updateById(member) == 1 ? true : false;
	}

	@Override
	public Integer getAccumulcateTime(String studentId) {
		String studentMemberId = orgStudentMapper.selectOne(
				new QueryWrapper<OrgStudentDetailed>().eq("id", studentId)).getMemberId();

		return memberMapper.selectOne(
				new QueryWrapper<Member>().eq("id", studentMemberId)).getAccuOnlineCourseConsumed();
	}

	@Override
	public Map getAvailTimeAndAccuTime(String studentId) {
		Map returnMap = new HashMap();

		try {
			returnMap.put("code", OnlineCourseConstants.SUCCESS);
			returnMap.put("availTime", getAvailTime(studentId));
			returnMap.put("accuTime", getAccumulcateTime(studentId));
		} catch (Exception e) {
			returnMap.put("code", OnlineCourseConstants.SERVER_ERROR);
			log.error(e.getMessage(), e);
			throw new BusinessException(e.getMessage());
		}

		return returnMap;
	}

	@Override
	public R getPeriodByDate(String studentId, String teacherId, String dateStr) {
		Map returnMap = new HashMap();
		Integer totalTime = 0;

		try {
			List<OnlineCoursePeriodVO> periodVOList = periodMapper.selectPeriodByDateStr(dateStr, studentId, teacherId);

			if (CollectionUtils.isEmpty(periodVOList)) {
				return new R(OnlineCourseEnum.PERIOD_NOT_FOUND);
			}

			for (OnlineCoursePeriodVO periodVO : periodVOList) {
				totalTime += periodVO.getTimeSpent() != null ? periodVO.getTimeSpent() : 0;
			}

			returnMap.put("periodList", periodVOList);
			returnMap.put("totalTime", totalTime);
		} catch (Exception e) {
			log.error(e.getMessage(), e);
			return new R(returnMap, OnlineCourseEnum.SERVER_ERROR);
		}

		return new R(returnMap, OnlineCourseEnum.SUCCESS);
	}

	@Override
	public R addSchedule(OnlineCourseScheduleVO requestVO) {
		try {
			Date date = new SimpleDateFormat("yyyy-MM-dd").parse(requestVO.getCourseDate());
			Calendar calendar = Calendar.getInstance();
			calendar.setTime(date);
			//1-7 周日到周六
			int dayOfWeekIdx = calendar.get(Calendar.DAY_OF_WEEK);

			String pureStartTime = toPureStartTime(requestVO.getStartTime());
			List<OnlineCourseScheduleVO> existing = scheduleMapper.selectByStartTime(
					requestVO.getCourseDate(), pureStartTime,
					requestVO.getTeacherId(), requestVO.getStudentId(), dayOfWeekIdx);

			if (existing != null && existing.size() != 0) {
				return new R(
						CommonConstants.FAIL, "添加失败，该开始时间已存在课程安排！", false);
			}

			requestVO.setCreateTime(LocalDateTime.now());
			requestVO.setNoOfWeek(dayOfWeekIdx);

			return new R(scheduleMapper.insert(requestVO));
		} catch (ParseException e) {
			log.error(e.getMessage(), e);
			return new R(CommonConstants.FAIL, "添加课程安排失败！", false);
		}
	}

	private String toPureStartTime(LocalDateTime startTime) {
		Date date = DateUtil.asDate(startTime);
		return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date).substring(11);
	}

	@Override
	public R editSchedule(OnlineCourseScheduleVO requestVO) {
		try {
			OnlineCourseSchedule existing = scheduleMapper.selectOne(
					new QueryWrapper<OnlineCourseSchedule>().eq("id", requestVO.getId()));

			if (existing == null) {
				return new R(CommonConstants.FAIL, "编辑失败，该开始时间不存在课程安排，请新增课程安排！", false);
			}

			//检查提醒类型是否改变
			if (!existing.getRemindType().equals(requestVO.getRemindType())) {
				existing.setRemindType(requestVO.getRemindType());
			}

			//检查学生ID是否改变
			if (!existing.getStudentId().equals(requestVO.getStudentId())) {
				existing.setStudentId(requestVO.getStudentId());
			}

			//检查时间和日期是否改变
			if (!existing.getCourseDate().equals(requestVO.getCourseDate()) || !existing.getStartTime()
					.equals(requestVO.getStartTime())) {
				OnlineCourseSchedule checkSingle = scheduleMapper.selectOne(new QueryWrapper<OnlineCourseSchedule>()
						.eq("course_date", requestVO.getCourseDate())
						.eq("start_time", requestVO.getStartTime())
						.eq("teacher_id", requestVO.getTeacherId())
						.eq("student_id", requestVO.getStudentId()));

				if (checkSingle != null) {
					return new R(CommonConstants.FAIL, "编辑失败，该时间已存在课程安排，请重新编辑！", false);
				}

				Date date = new SimpleDateFormat("yyyy-MM-dd").parse(requestVO.getCourseDate());
				Calendar calendar = Calendar.getInstance();
				calendar.setTime(date);
				//1-7 周日到周六
				int newDayOfWeekIdx = calendar.get(Calendar.DAY_OF_WEEK);

				String pureStartTime = toPureStartTime(requestVO.getStartTime());
				List<OnlineCourseScheduleVO> checkRepeatable = scheduleMapper.selectByStartTime(
						requestVO.getCourseDate(), pureStartTime,
						requestVO.getTeacherId(), requestVO.getStudentId(), newDayOfWeekIdx);
				if (checkRepeatable.size() != 0) {
					return new R(CommonConstants.FAIL, "编辑失败，该开始时间已存在课程安排，请重新编辑！", false);
				}

				existing.setStartTime(requestVO.getStartTime());
				existing.setCourseDate(requestVO.getCourseDate());
			}

			existing.setUpdateTime(LocalDateTime.now());

			return new R(scheduleMapper.updateById(existing));
		} catch (Exception e) {
			log.error(e.getMessage(), e);
			return new R(CommonConstants.FAIL, "编辑课程安排失败！", false);
		}
	}

	@Override
	public R deleteSchedule(OnlineCourseScheduleVO requestVO) {
		return new R(scheduleMapper.delete(new QueryWrapper<OnlineCourseSchedule>().eq("id", requestVO.getId())));
	}

	@Override
	public R getScheduleMonthly(Integer year, Integer month, String teacherId, String studentId) {
		//此处逻辑为，如果取到某日的该时间段提醒类型为---每周或者每日
		//则此课程表数据只会在数据库插入一条，取的时候按该时间段最新的l提醒类型配置去取
		SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
		String paramMonth = month < 10 ? "0" + month : String.valueOf(month);

		try {
			List<OnlineCourseScheduleVO> scheduleVOList =
					scheduleMapper.selectScheduleMonthly(year, paramMonth, teacherId, studentId);

			List<OnlineCourseScheduleVO> tmpAddList = new ArrayList<>();

			//TODO
			for (OnlineCourseScheduleVO scheduleVO : scheduleVOList) {
				if (ScheduleRemindTypeEnum.EACH_DAY.getCode().equals(scheduleVO.getRemindType())) {
					//计算当月的第一天
					Date date = DateUtil.getFirstDayOfMonth(year, month);
					Integer monthDays = DateUtil.getDaysByYearMonth(year, month);

					for (int i = 0; i < monthDays; i++) {
						OnlineCourseScheduleVO copyBean = new OnlineCourseScheduleVO();
						BeanUtils.copyProperties(scheduleVO, copyBean);
						copyBean.setCourseDate(format.format(DateUtil.addDays(date, i)));
						tmpAddList.add(copyBean);
					}
				} else if (ScheduleRemindTypeEnum.EACH_WEEK.getCode().equals(scheduleVO.getRemindType())) {
					//获取该月所有的周几
					List<LocalDate> weeDaysList =
							DateUtil.getAllWeekDays(year, month, DateUtil.getDayOfWeek(scheduleVO.getCourseDate(), format));

					for (LocalDate date : weeDaysList) {
						OnlineCourseScheduleVO copyBean = new OnlineCourseScheduleVO();
						BeanUtils.copyProperties(scheduleVO, copyBean);
						copyBean.setCourseDate(date.toString());
						tmpAddList.add(copyBean);
					}
				}
			}

			tmpAddList.addAll(scheduleVOList);

			return new R(scheduleVOList);
		} catch (Exception e) {
			log.error(e.getMessage(), e);
			return new R(CommonConstants.FAIL, "查询时发生错误", false);
		}
	}

	@Override
	public R getScheduleWeekly(String mondayDateStr, String teacherId, String studentId) {
		SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");

		try {
			Calendar calendar = Calendar.getInstance();
			calendar.setTime(format.parse(mondayDateStr));

			//得到周日时间
			calendar.add(Calendar.DAY_OF_MONTH, -1);

			String sundayDateStr = format.format(new Date(calendar.getTimeInMillis()));

			calendar.add(Calendar.DAY_OF_MONTH, 6);

			String nextMondayDateStr = format.format(calendar.getTime());

			List<OnlineCourseScheduleVO> scheduleVOList =
					scheduleMapper.selectScheduleWeekly(sundayDateStr, nextMondayDateStr, teacherId, studentId);
			List<OnlineCourseScheduleVO> tmpAddList = new ArrayList<>();

			for (OnlineCourseScheduleVO scheduleVO : scheduleVOList) {
				if (ScheduleRemindTypeEnum.EACH_DAY.getCode().equals(scheduleVO.getRemindType())) {
					Date mondayDate = format.parse(mondayDateStr);

					for (int i = -1; i < 6; i++) {
						if (i == 0) {
							continue;
						}
						Date tmpDate = DateUtil.addDays(mondayDate, i);

						OnlineCourseScheduleVO copyBean = new OnlineCourseScheduleVO();
						BeanUtils.copyProperties(scheduleVO, copyBean);
						copyBean.setCourseDate(format.format(tmpDate));
						tmpAddList.add(copyBean);
					}
				}
			}

			tmpAddList.addAll(scheduleVOList);

			return new R(scheduleVOList);
		} catch (Exception e) {
			log.error(e.getMessage(), e);
			return new R(CommonConstants.FAIL, "查询时发生错误", false);
		}
	}

	@Override
	public Map getStudentInfo(String studentId, String teacherId, Integer type) {
		Map returnMap = new HashMap();

		try {
			if (type == 0) {
				Integer orgAvailTime = getOrgAvailTime(teacherId);

				if (orgAvailTime == -1) {
					returnMap.put("code", OnlineCourseConstants.TEACHER_NOT_FOUND);
					returnMap.put("msg", "未找到老师信息!");
					return returnMap;
				}

				if (orgAvailTime == -2) {
					returnMap.put("code", OnlineCourseConstants.TEACHER_NO_ORG);
					returnMap.put("msg", "老师无归属机构或工作室");
					return returnMap;
				}

				returnMap.put("orgAvailTime", orgAvailTime);
			}

			returnMap.put("code", OnlineCourseConstants.SUCCESS);
			returnMap.put("accuTime", getAccumulcateTime(studentId));
		} catch (Exception e) {
			returnMap.put("code", OnlineCourseConstants.SERVER_ERROR);
			log.error(e.getMessage(), e);
			throw new BusinessException(e.getMessage());
		}

		return returnMap;
	}

	@Override
	public Integer getOrgAvailTime(String teacherId) {
		OrgTeacherDetailed orgTeacher = orgTeacherMapper.selectOne(
				new QueryWrapper<OrgTeacherDetailed>().eq("id", teacherId));
		if (orgTeacher == null) {
			return -1;
		}

		OrgOrganization org = organMapper.selectOne(
				new QueryWrapper<OrgOrganization>().eq("id", orgTeacher.getOrgId()));

		if (org == null) {
			return -2;
		}

		return org.getOnlineCourseAvail();
	}

	@Override
	public List<OnlineCourseRecordVO> getCourseDetailRecord(String studentId, String teacherId, Integer year, Integer month) {
		String paramMonth = month < 10 ? "0" + month : String.valueOf(month);

		List<OnlineCourseRecordVO> recordList =
				recordMapper.selectAllTeacherStudentRecordsByMonth(year, paramMonth, studentId, teacherId);
		for (OnlineCourseRecordVO recordVO : recordList) {
			List<OnlineCoursePeriodVO> periodVOList = periodMapper.selectPeriodByDateStr(
					recordVO.getCourseDate(), studentId, teacherId);

			recordVO.setPeriodList(periodVOList);

			Integer totalTime = 0;

			for (OnlineCoursePeriodVO periodVO : periodVOList) {
				totalTime += periodVO.getTimeSpent() != null ? periodVO.getTimeSpent() : 0;
			}

			recordVO.setTotalTime(totalTime);
		}

		return recordList;
	}

	@Override
	public Map pageCoursePeriodByTimeDesc(Page<OnlineCoursePeriodVO> page, String studentId, String teacherId) {
		TreeMap<String, Map> returnMap = null;
		SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM");

		try {
			returnMap = new TreeMap<String, Map>(new Comparator<String>() {
				@Override
				public int compare(String dateStr1, String dateStr2) {
					Date date1 = null;
					Date date2 = null;
					try {
						date1 = formatter.parse(dateStr1);
						date2 = formatter.parse(dateStr2);
					} catch (ParseException e) {
						log.error("日期解析错误, 日期字符串1为: " + dateStr1);
						log.error("日期解析错误, 日期字符串2为: " + dateStr2);
						log.error(e.getMessage(), e);
					}
					return date2.compareTo(date1);
				}
			});

			IPage<OnlineCoursePeriodVO> periodIPage = periodMapper.pageAllPeriodByTimeDesc(page, studentId, teacherId);
			List<OnlineCoursePeriodVO> periodList = periodIPage.getRecords();

			for (OnlineCoursePeriodVO periodVO : periodList) {
				String dateStr = periodVO.getCourseDate();
				String[] timeArr = dateStr.split("-");
				Integer year = Integer.valueOf(timeArr[0]);
				Integer month = Integer.valueOf(timeArr[1]);
				Integer day = Integer.valueOf(timeArr[2]);

				String yearMonth = year + "-" + month;
				if (returnMap.containsKey(yearMonth)) {
					Map<Integer, List<OnlineCoursePeriodVO>> tmpPeriodMap = (Map) returnMap.get(yearMonth);
					if (tmpPeriodMap.containsKey(day)) {
						List<OnlineCoursePeriodVO> tmpPeriodList = (List) tmpPeriodMap.get(day);
						tmpPeriodList.add(periodVO);
						tmpPeriodMap.put(day, tmpPeriodList);
					} else {
						List<OnlineCoursePeriodVO> tmpPeriodList = new ArrayList<>();
						tmpPeriodList.add(periodVO);
						tmpPeriodMap.put(day, tmpPeriodList);
					}

					returnMap.put(yearMonth, tmpPeriodMap);
				} else {
					Map<Integer, List<OnlineCoursePeriodVO>> tmpPeriodMap =
							new TreeMap<Integer, List<OnlineCoursePeriodVO>>(new Comparator<Integer>() {
								@Override
								public int compare(Integer day1, Integer day2) {
									return day2.compareTo(day1);
								}
							});

					List<OnlineCoursePeriodVO> tmpPeriodList = new ArrayList<>();
					tmpPeriodList.add(periodVO);
					tmpPeriodMap.put(day, tmpPeriodList);
					returnMap.put(yearMonth, tmpPeriodMap);
				}
			}

			return returnMap;
		} catch (Exception e) {
			log.error(e.getMessage(), e);
			return returnMap;
		}
	}

	@Override
	public OnlineCourseTrialInfoReturnVO getTrialInfo(String memberId) {
		Member member = memberMapper.selectOne(new QueryWrapper<Member>().eq("id", memberId));

		OnlineCourseTrialInfoReturnVO returnVO = new OnlineCourseTrialInfoReturnVO();

		returnVO.setOnlineCourseTrialTime(member.getOnlineCourseTrialTime());
		returnVO.setOnlineCourseTrialStatus(member.getOnlineCourseTrialStatus());
		returnVO.setOnlineCourseTrialBonusStatus(member.getOnlineCourseTrialBonusStatus());

		return returnVO;
	}

	@Override
	public R claimTrial(String memberId) {
		Member member = memberMapper.selectOne(new QueryWrapper<Member>().eq("id", memberId));

		if (member == null) {
			return new R(CommonConstants.FAIL, "未找到该用户！", false);
		}

		if (1 == member.getOnlineCourseTrialStatus()) {
			return new R(CommonConstants.FAIL, "用户已领取！", false);
		}

		member.setOnlineCourseTrialStatus(1);
		memberMapper.updateById(member);

		//增加机构课时60分钟
//		OrgTeacherDetailed teacherEntity = orgTeacherMapper.selectOne(
//				new QueryWrapper<OrgTeacherDetailed>().eq("member_id", memberId));
//		OrgStudentDetailed studentEntity = orgStudentMapper.selectOne(
//				new QueryWrapper<OrgStudentDetailed>().eq("member_id", memberId));
//
//		if (teacherEntity != null || studentEntity !=null) {
//			Integer orgId = teacherEntity != null ? teacherEntity.getOrgId() : studentEntity.getOrgId();
//			OrgOrganization orgEntity = organMapper.selectOne(
//					new QueryWrapper<OrgOrganization>().eq("id", orgId));
//			orgEntity.setOnlineCourseAvail(orgEntity.getOnlineCourseAvail() + 60);
//		}

		return new R(OnlineCourseConstants.SUCCESS, "领取成功！", true);
	}

	@Override
	public R consumeTrialTime(String memberId, Integer consumed) {
		Member member = memberMapper.selectOne(new QueryWrapper<Member>().eq("id", memberId));

		if (member == null) {
			return new R(CommonConstants.FAIL, "未找到该用户！", false);
		}

		member.setOnlineCourseTrialTime(member.getOnlineCourseTrialTime() - consumed);
		int updateResult = memberMapper.updateById(member);

		return updateResult == 1 ? new R(OnlineCourseConstants.SUCCESS, "体验时间扣除成功！", true)
				: new R(OnlineCourseConstants.SERVER_ERROR, "体验时间扣除失败！", false);
	}

	@Override
	public R claimTrialBonus(String memberId) {
		Member member = memberMapper.selectOne(new QueryWrapper<Member>().eq("id", memberId));

		if (member == null) {
			return new R(CommonConstants.FAIL, "未找到该用户！", false);
		}

		if (1 == member.getOnlineCourseTrialBonusStatus()) {
			return new R(CommonConstants.FAIL, "用户已领取！", false);
		}

		member.setOnlineCourseTrialTime(member.getOnlineCourseTrialTime() + 30);
		member.setOnlineCourseTrialBonusStatus(1);

		memberMapper.updateById(member);

		//额外增加机构课时30分钟
//		OrgTeacherDetailed teacherEntity = orgTeacherMapper.selectOne(
//				new QueryWrapper<OrgTeacherDetailed>().eq("member_id", memberId));
//		OrgStudentDetailed studentEntity = orgStudentMapper.selectOne(
//				new QueryWrapper<OrgStudentDetailed>().eq("member_id", memberId));
//
//		if (teacherEntity != null || studentEntity !=null) {
//			Integer orgId = teacherEntity != null ? teacherEntity.getOrgId() : studentEntity.getOrgId();
//			OrgOrganization orgEntity = organMapper.selectOne(
//					new QueryWrapper<OrgOrganization>().eq("id", orgId));
//			orgEntity.setOnlineCourseAvail(orgEntity.getOnlineCourseAvail() + 30);
//		}

		return new R(OnlineCourseConstants.SUCCESS, "领取成功！", true);
	}

	@Override
	public R createTrialRoom(String teacherMemberId, OnlineCourseRequestVO requestVO) {
		try {
			String roomId = getTrialRoomId(requestVO.getTrialRoomShowId());

			redisTemplate.opsForValue().set(
					roomId, System.currentTimeMillis(), 86400, TimeUnit.SECONDS);

			redisTemplate.opsForValue().set(
					roomId + ": trialPw", requestVO.getTrialPassword(), 86400, TimeUnit.SECONDS);

			return new R(OnlineCourseConstants.SUCCESS, "创建房间成功", true);
		} catch (Exception e) {
			log.error(e.getMessage(), e);
			return new R(OnlineCourseConstants.SERVER_ERROR, "创建房间失败", false);
		}
	}

	@Override
	public R checkTrialRoom(String memberId, OnlineCourseRequestVO requestVO) {
		AccessRoomReturnVO returnVO = new AccessRoomReturnVO();

		if (requestVO.getTrialRoomShowId() == null) {
			return new R(OnlineCourseConstants.PARAMS_CANNOT_BE_EMPTY, "房间号参数不能为空", false);
		}

		String roomId = getTrialRoomId(requestVO.getTrialRoomShowId());

		if (!redisTemplate.hasKey(roomId)) {
			returnVO.setFailed("房间不存在!");
			return new R(returnVO);
		}

		//验证学生密码
		Integer password = (Integer) redisTemplate.opsForValue().get(roomId + ": trialPw");
		if (!password.equals(requestVO.getTrialPassword())) {
			returnVO.setFailed("房间密码错误!");
			return new R(returnVO);
		}

		returnVO.setSuccess("学生成功进入房间", 1, SecurityUtils.getMember().getId());

		return new R(returnVO);
	}

	@Override
	public R getTrialRoomPassword(String trialRoomShowId) {
		try {
			String roomId = getTrialRoomId(trialRoomShowId);

			if (!redisTemplate.hasKey(roomId)) {
				return new R(OnlineCourseConstants.ROOM_NOT_FOUND, "房间不存在", false);
			}

			return new R((Integer) redisTemplate.opsForValue().get(roomId + ": trialPw"));
		} catch (Exception e) {
			log.error(e.getMessage(), e);
			return new R(OnlineCourseConstants.SERVER_ERROR, "获取密码失败", false);
		}
	}

	@Override
	public RoomInfo generateRTCTokenAndRoomInfo() {
		String memberId = SecurityUtils.getMember().getId();
		String channelName = System.nanoTime() + "_" + memberId;
		String token = RTCUtil.genRTCToken(channelName);

		RoomInfo roomInfo = new RoomInfo();
		roomInfo.setToken(token);
		roomInfo.setChannel(channelName);

		return roomInfo;
	}

	private TrialRoomInfo generateTrialRTCTokenAndRoomInfo() {
		String memberId = SecurityUtils.getMember().getId();
		String channelName = System.nanoTime() + "_" + memberId;
		String token = RTCUtil.genRTCToken(channelName);

		TrialRoomInfo roomInfo = new TrialRoomInfo();
		roomInfo.setToken(token);
		roomInfo.setChannel(channelName);

		return roomInfo;
	}

	@Override
	public String checkIfHasToken(String teacherId, String studentId, Integer type) {
		String concatKey = getRoomId(type, studentId, teacherId) + ":" + (type == 0 ? studentId : teacherId);

		return redisTemplate.hasKey(concatKey) ? String.valueOf(redisTemplate.opsForValue().get(concatKey)) : null;
	}

//	@Override
//	public String checkLeavingStatus(String teacherId, String studentId, Integer type) {
//		String concatKey = getRoomId(type, studentId, teacherId) + ":" + (type == 0 ? studentId : teacherId);
//
//		return redisTemplate.hasKey(concatKey) ? String.valueOf(redisTemplate.opsForValue().get(concatKey)) : null;
//	}

	@Override
	public R resetHeartBeatData(OnlineCourseRequestVO requestVO) {
		String studentId = requestVO.getStudentId();
		String teacherId = requestVO.getTeacherId();
		String roomId = getRoomId(0, studentId, teacherId);

		String teaPrefix = roomId + ":t_";
		String stuPrefix = roomId + ":s_";
		String teaLastBeatKey = teaPrefix + "_last_beat";
		String stuLastBeatKey = stuPrefix + "_last_beat";
		String teaCurBeatKey = teaPrefix + "_cur_beat";
		String stuCurBeatKey = stuPrefix + "_cur_beat";

		redisTemplate.delete(teaLastBeatKey);
		redisTemplate.delete(stuLastBeatKey);
		redisTemplate.delete(teaCurBeatKey);
		redisTemplate.delete(stuCurBeatKey);

		String todayStr = new SimpleDateFormat("yyyy-MM-dd").format(new Date());

		QueryWrapper infoWrapper = new QueryWrapper<OnlineCourseInfo>().eq("course_date", todayStr);
		OnlineCourseInfo info = infoMapper.selectOne(infoWrapper);
		if (info != null) {
			Long courseId = info.getId();
			infoMapper.delete(infoWrapper);
			QueryWrapper periodWrapper = new QueryWrapper<OnlineCoursePeriod>().eq("course_id", courseId);
			periodMapper.delete(periodWrapper);
		}

		return new R(CommonConstants.SUCCESS, "重置成功!", true);
	}

	private void startConsumeOnlineCourse(String teacherId, String studentId, String roomId, Integer type) throws Exception {
		OnlineCourseInfo infoEntity = null;
		String todayStr = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
		List<OnlineCourseInfo> infoEntityList = infoMapper.selectList(
				new QueryWrapper<OnlineCourseInfo>().eq("teacher_id", teacherId)
						.eq("student_id", studentId)
						.eq("course_date", todayStr)
						.orderByAsc("create_time"));

		//存入分段开始时间
		OnlineCoursePeriod periodEntity = new OnlineCoursePeriod();
		periodEntity.setStartTime(LocalDateTime.now());

		if (infoEntityList == null || infoEntityList.size() == 0) {
			//生成在线课记录，生成courseId
			infoEntity = new OnlineCourseInfo();
			infoEntity.setCourseDate(new SimpleDateFormat("yyyy-MM-dd").format(new Date()));
			infoEntity.setTeacherId(teacherId);
			infoEntity.setStudentId(studentId);
			infoEntity.setCreateTime(LocalDateTime.now());
			infoMapper.insert(infoEntity);

			periodEntity.setCourseId(infoEntity.getId());
		} else {
			periodEntity.setCourseId(infoEntityList.get(0).getId());
		}

		redisTemplate.opsForValue().set(
				roomId + ":" + "startTime", System.currentTimeMillis(), 86400, TimeUnit.SECONDS);
		periodMapper.insert(periodEntity);

		String userPrefix = roomId + (type == 0 ? ":t_" : ":s_");
		String lastBeatKey = userPrefix + "_last_beat";

		//记录最后一次心跳房间的timeStamp
		redisTemplate.opsForValue().set(lastBeatKey,
				System.currentTimeMillis(), 86400, TimeUnit.SECONDS);
	}

	private String getRoomId(Integer type, String studentId, String teacherId) {
		if (type == 1 && teacherId == null) {
			//找到自己的老师，一个学生目前只能有一个老师
			teacherId = getTeacherId(studentId);
		}

		String todayStr = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
		return OnlineCourseConstants.REDIS_PREFIX + "_" + todayStr + ":" + teacherId.substring(0, 5) + "_" + studentId.substring(0, 5);
	}

	//当前getTrialRoomShowId是老师手机号
	private String getTrialRoomId(String getTrialRoomShowId) {
		String todayStr = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
		return OnlineCourseConstants.REDIS_TRIAL_PREFIX + "_" + todayStr + ":" + getTrialRoomShowId;
	}

	private String getTeacherId(String studentId) {
		return teacherStudentMapper.selectOne(
				new QueryWrapper<TeacherStudentEntity>()
						.eq("student_id", studentId)).getTeacherId();
	}

	private String getStudentId(String teacherId) {
		return teacherStudentMapper.selectOne(
				new QueryWrapper<TeacherStudentEntity>()
						.eq("teacher_id", teacherId)).getStudentId();
	}

	//处理双方第一次心跳
	@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
	protected R processFirstBeat(int type, String roomId, String studentId, String teacherId, Integer beatIdx,
			Long curTime, String stuLastBeatKey, String teaLastBeatKey,
			Long teaLastCheckPoint, Long stuLastCheckPoint, Long prevStartTime) throws Exception {
		String todayStr = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
		if (type == 0 && beatIdx == 1) {
			//更新第一次心跳时间戳
			redisTemplate.opsForValue().set(teaLastBeatKey, curTime, 86400, TimeUnit.SECONDS);

			//说明是老师，老师已进入房间，然后只需要判断学生是否已经进入并且已收到学生的第一个心跳
			if (redisTemplate.hasKey(roomId + ":s_" + studentId) && redisTemplate.hasKey(stuLastBeatKey)) {
				Boolean unlocked = redisTemplate.opsForValue().setIfAbsent(
						roomId + ":" + "periodLock", System.currentTimeMillis(), 20, TimeUnit.SECONDS);

				//加入分布式锁
				if (unlocked) {
					checkExisting(todayStr, teacherId, studentId, teaLastCheckPoint, stuLastCheckPoint, prevStartTime);
					startConsumeOnlineCourse(teacherId, studentId, roomId, type);
					redisTemplate.delete(roomId + ":" + "shortDeleted");
				}

				return new R(CommonConstants.SUCCESS, "老师学生心跳均已收到，开始上课计时",
						new HeartBeatReturnVO(beatIdx, true, teacherId, studentId));
			}
			return new R(CommonConstants.SUCCESS, " 已注册老师第一次心跳",
					new HeartBeatReturnVO(beatIdx, true, teacherId, studentId));
		} else if (type == 1 && beatIdx == 1) {
			//更新第一次心跳时间戳
			redisTemplate.opsForValue().set(stuLastBeatKey, curTime, 86400, TimeUnit.SECONDS);

			//说明是学生，学生已进入房间，然后只需要判断老师是否已经进入并且已收到老师的第一个心跳
			if (redisTemplate.hasKey(roomId + ":t_" + teacherId) && redisTemplate.hasKey(teaLastBeatKey)) {
				Boolean unlocked = redisTemplate.opsForValue().setIfAbsent(
						roomId + ":" + "periodLock", System.currentTimeMillis(), 20, TimeUnit.SECONDS);

				if (unlocked) {
					checkExisting(todayStr, teacherId, studentId, teaLastCheckPoint, stuLastCheckPoint, prevStartTime);
					startConsumeOnlineCourse(teacherId, studentId, roomId, type);
					redisTemplate.delete(roomId + ":" + "shortDeleted");
				}

				return new R(CommonConstants.SUCCESS, "老师学生心跳均已收到，开始上课计时",
						new HeartBeatReturnVO(beatIdx, true, teacherId, studentId));
			}
			return new R(CommonConstants.SUCCESS, "已注册学生第一次心跳",
					new HeartBeatReturnVO(beatIdx, true, teacherId, studentId));
		}

		return null;
	}

	private void checkExisting(String todayStr, String teacherId, String studentId,
			Long stuLastCheckPoint, Long teaLastCheckPoint,
			Long prevStartTime) throws Exception {
		if (stuLastCheckPoint == null && teaLastCheckPoint == null) {
			return;
		}

		Long periodEndTime = stuLastCheckPoint == null ?
				teaLastCheckPoint : (teaLastCheckPoint == null ?
				stuLastCheckPoint : Math.min(stuLastCheckPoint, teaLastCheckPoint));

		if (prevStartTime == null) {
			return;
		}

		if (DateUtil.getTimestampMinDiffFloor(System.currentTimeMillis(), prevStartTime) == 0) {
			periodMapper.deleteMostRecentRecordByUsers(todayStr, studentId, teacherId);
		} else {
			if (DateUtil.getTimestampMinDiffFloor(periodEndTime, prevStartTime) == 0) {
				periodMapper.deleteMostRecentRecordByUsers(todayStr, studentId, teacherId);
			} else {
				periodMapper
						.updateMostRecentEndTimeByDateAndUsers(DateUtil.toLocalDateTime(new Date(periodEndTime)), todayStr, studentId,
								teacherId);
			}
		}
	}

	//计算与上一次心跳的差值
	@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
	protected R calculateDiff(int type, Integer beatIdx, String teacherId, String studentId, String teaTmpBeatKey,
			Long teaLastCheckPoint, String teaLastBeatKey, Long curTime, Long curCheckPoint,
			String stuCurBeatKey, String teaCurBeatKey, String stuTmpBeatKey, Long stuLastCheckPoint, String stuLastBeatKey,
			BeatDataDTO beatDTO, Boolean isTrail) throws Exception {
		//老师逻辑
		if (type == 0) {
			//体验课OrgId设置为888888
			beatDTO.setOrgId(isTrail ? 888888 : orgTeacherMapper.selectOne(
					new QueryWrapper<OrgTeacherDetailed>().eq("id", teacherId)).getOrgId());

			//将老师上一次心跳缓存到临时的BeatKey里
			redisTemplate.opsForValue().set(teaTmpBeatKey, teaLastCheckPoint, 29, TimeUnit.SECONDS);
			//更新老师上一次心跳时间戳
			redisTemplate.opsForValue().set(teaLastBeatKey, curTime, 86400, TimeUnit.SECONDS);

			//计算相差的分钟数
			beatDTO.setTeaMinDiff(DateUtil.getTimestampMinDiff(curCheckPoint, teaLastCheckPoint));

			Long stuCurCheckPoint = (Long) redisTemplate.opsForValue().get(stuCurBeatKey);
			if (stuCurCheckPoint == null) {
				//说明学生那边还没有收到心跳，则返回
				//更新当前时间戳
				redisTemplate.opsForValue().set(teaCurBeatKey, curTime, 29, TimeUnit.SECONDS);

				return new R(CommonConstants.SUCCESS, "教师心跳更新成功，等待对应老师心跳",
						new HeartBeatReturnVO(beatIdx, true, teacherId, studentId));
			} else {
				//上上次学生缓存的心跳时间戳
				Long stuTmpCheckPoint = (Long) redisTemplate.opsForValue().get(stuTmpBeatKey);
				//说明学生收到心跳，获取老师当前心跳时间戳
				beatDTO.setStuMinDiff(DateUtil.getTimestampMinDiff(stuCurCheckPoint, stuTmpCheckPoint));
			}
			//学生逻辑
		} else {
			//体验课OrgId设置为888888
			beatDTO.setOrgId(isTrail ? 888888 : orgStudentMapper.selectOne(
					new QueryWrapper<OrgStudentDetailed>().eq("id", studentId)).getOrgId());

			//将学生上一次心跳缓存到临时的BeatKey里
			redisTemplate.opsForValue().set(stuTmpBeatKey, stuLastCheckPoint, 29, TimeUnit.SECONDS);
			//更新学生上一次心跳时间戳
			redisTemplate.opsForValue().set(stuLastBeatKey, curTime, 86400, TimeUnit.SECONDS);

			//计算相差的分钟数
			beatDTO.setStuMinDiff(DateUtil.getTimestampMinDiff(curCheckPoint, stuLastCheckPoint));

			Long teaCurCheckPoint = (Long) redisTemplate.opsForValue().get(teaCurBeatKey);
			if (teaCurCheckPoint == null) {
				//说明老师那边还没有收到心跳，则返回
				//更新当前时间戳
				redisTemplate.opsForValue().set(stuCurBeatKey, curTime, 29, TimeUnit.SECONDS);

				return new R(CommonConstants.SUCCESS, "学生心跳更新成功，等待对应教师心跳",
						new HeartBeatReturnVO(beatIdx, true, teacherId, studentId));
			} else {
				//上上次老师缓存的心跳时间戳
				Long teaTmpCheckPoint = (Long) redisTemplate.opsForValue().get(stuTmpBeatKey);
				//说明老师收到心跳，获取老师当前心跳时间戳
				beatDTO.setTeaMinDiff(DateUtil.getTimestampMinDiff(teaCurCheckPoint, teaTmpCheckPoint));
			}
		}

		return null;
	}

	@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
	protected R consumeTime(Integer type, BeatDataDTO beatDTO, String memberId,
			String teacherId, String studentId, Integer beatIdx) throws Exception {
		//消耗一分钟在线课课时
		Integer delta = 1;

		if (beatDTO.getOrgId() == null) {
			throw new BusinessException("未找到对应机构，在线课时长更新失败!");
		}
		OrgOrganization org = organMapper.selectOne(
				new QueryWrapper<OrgOrganization>().eq("id", beatDTO.getOrgId()));

		//消耗机构可用时长
		Integer countAfterCal = org.getOnlineCourseAvail() - delta;

		//暂时去除限制逻辑，目前由前端根据可用时长，限制用户进入教室(若为负数则不让进入教室)
//		if (countAfterCal < Integer.valueOf(propertiesConfig.getOnlineCourseMinAvailTimeThreshold())) {
//			throw new BusinessException("已欠在线课时长" + countAfterCal + ", 请充值购买在线课时长后方可继续上课！");
//		}

		org.setOnlineCourseAvail(countAfterCal);
		if (organMapper.updateById(org) != 1) {
			throw new BusinessException("心跳在线课时长更新失败");
		}

		//更新stock_quantity
		BasicGoodsInfo online = goodsInfoMapper.selectOne(
				new QueryWrapper<BasicGoodsInfo>().like("name", "在线课"));
		StockQuantity onlineQuantity = stockQuantityMapper.selectOne(
				new QueryWrapper<StockQuantity>().eq("goods_id", online.getId()).eq("org_id", beatDTO.getOrgId()));
		if (onlineQuantity != null && onlineQuantity.getGoodsQuantity() != null) {
			countAfterCal = onlineQuantity.getGoodsQuantity() - delta / online.getUnitCount();

			//TODO 此处暂不检查是否小于threshold

			onlineQuantity.setGoodsQuantity(onlineQuantity.getGoodsQuantity() - delta / online.getUnitCount());

			//这里有问题，因为在线课单位时长可能是会发生变化的，所以不能按照商品数量这种逻辑来存储，
			//这里我们先更新StockQuantity,但是实际上我们获取和计算用OrgOrganization的onlineCourseAvail这个字段
			if (stockQuantityMapper.updateById(onlineQuantity) != 1) {
				throw new BusinessException("心跳在线课时长更新失败");
			}
		}

		//消耗当前用户可用时长
		Member member = memberMapper.selectOne(new QueryWrapper<Member>().eq("id", memberId));
		if (null == member) {
			throw new BusinessException("未找到当前会员信息!");
		}
		member.setOnlineCourseValidTime(member.getOnlineCourseValidTime() - delta);
		member.setAccuOnlineCourseConsumed(member.getAccuOnlineCourseConsumed() + delta);
		memberMapper.updateById(member);

		//消耗对方用户可用时长
		String otherSideMemberId = (type == 0) ?
				orgStudentMapper.selectOne(new QueryWrapper<OrgStudentDetailed>().eq("id", studentId)).getMemberId() :
				orgTeacherMapper.selectOne(new QueryWrapper<OrgTeacherDetailed>().eq("id", teacherId)).getMemberId();
		Member otherSideMember = memberMapper.selectOne(new QueryWrapper<Member>().eq("id", otherSideMemberId));
		if (null == otherSideMember) {
			throw new BusinessException("未找到对方会员信息!");
		}
		otherSideMember.setOnlineCourseValidTime(otherSideMember.getOnlineCourseValidTime() - delta);
		otherSideMember.setAccuOnlineCourseConsumed(otherSideMember.getAccuOnlineCourseConsumed() + delta);
		memberMapper.updateById(otherSideMember);

		return new R(CommonConstants.SUCCESS, "成功扣除学生和老师在线课时长1分钟",
				new HeartBeatReturnVO(beatIdx, true, teacherId, studentId));
	}

	@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
	protected R processTimeout(Integer beatIdx, Long stuLastCheckPoint, Long teaLastCheckPoint, BeatDataDTO beatDTO,
			String teacherId, String studentId) throws Exception {
		String todayStr = new SimpleDateFormat("yyyy-MM-dd").format(new Date());

		List<OnlineCourseInfo> infoEntityList = infoMapper.selectList(
				new QueryWrapper<OnlineCourseInfo>().eq("course_date", todayStr)
						.eq("student_id", studentId)
						.eq("teacher_id", teacherId).orderByAsc("create_time"));

		if (infoEntityList == null || infoEntityList.size() == 0) {
			//说明没有课程信息，按理来说逻辑走不到这里，TODO
			return new R(CommonConstants.SUCCESS, beatDTO.getStuMinDiff() > 20 ? "学生" : "老师" + "和上次心跳相隔超过20分钟，" +
					"将会以上次学生和老师更早的心跳作为此段课程结束时间，并重新开启下一段的开始时间", new HeartBeatReturnVO(beatIdx, true, teacherId, studentId));
		} else {
			Long courseId = infoEntityList.get(0).getId();

			//拿到最新的一段记录，设置endTime为双方上一次更早的心跳时间
			Long periodEndTime = Math.min(stuLastCheckPoint, teaLastCheckPoint);
			periodMapper.updateMostRecentEndTimeByDate(DateUtil.toLocalDateTime(new Date(periodEndTime)), todayStr, courseId);

			//重新开始当天的一段新的记录
			//存入分段开始时间
			OnlineCoursePeriod periodEntity = new OnlineCoursePeriod();
			periodEntity.setCourseId(courseId);
			periodEntity.setStartTime(LocalDateTime.now());
			periodMapper.insert(periodEntity);
		}

		return new R(CommonConstants.SUCCESS, beatDTO.getStuMinDiff() > 20 ? "学生" : "老师" + "和上次心跳相隔超过20分钟，" +
				"将会以上次学生和老师更早的心跳作为此段课程结束时间，并重新开启下一段的开始时间", new HeartBeatReturnVO(beatIdx, true, teacherId, studentId));
	}

	@Override
	public List<AppPayOnlineCourseVO> getGoodsList() {

		List<AppPayOnlineCourseVO> res = new ArrayList<>();

		//获取商品列表 product 固定为 12
		List<BasicGoodsInfo> basicGoodsInfos = goodsInfoMapper.selectList(new QueryWrapper<BasicGoodsInfo>()
				.eq("product_id", 12)
				.eq("status", "0")
				.orderByAsc("sort"));

		for (BasicGoodsInfo basicGoodsInfo : basicGoodsInfos) {

			AppPayOnlineCourseVO appPayOnlineCourseVO = new AppPayOnlineCourseVO();

			appPayOnlineCourseVO.setId(basicGoodsInfo.getId());
			appPayOnlineCourseVO.setName(basicGoodsInfo.getName());
			appPayOnlineCourseVO.setBuyDuration(basicGoodsInfo.getUnitCount());
			appPayOnlineCourseVO.setIosPrice(basicGoodsInfo.getIosPrice());
			appPayOnlineCourseVO.setPrice(basicGoodsInfo.getGoodsPrice());
			appPayOnlineCourseVO.setIosCode(basicGoodsInfo.getIosCode());
			appPayOnlineCourseVO.setGiveDuration(0);
			//判断是否有赠送
			if (basicGoodsInfo.getGiftGoodsId() != null) {

				BasicGoodsInfo giveGoods = goodsInfoMapper.selectById(basicGoodsInfo.getGiftGoodsId());
				if (giveGoods == null || "1".equals(giveGoods.getStatus())) {
					appPayOnlineCourseVO.setGiveDuration(0);
				} else {
					appPayOnlineCourseVO.setGiveDuration(giveGoods.getUnitCount());
				}

			}

			res.add(appPayOnlineCourseVO);

		}

		return res;
	}

	@Override
	public List<TeacherCourseVO> getOrgTeacherCourseList(Integer orgId) {
		return infoMapper.getOrgTeacherCourseList(orgId);
	}

	@Override
	public IPage<OnlineCoursePeriodVO> getTeacherCourseDetail(Page page, String teacherId) {
		return infoMapper.getTeacherCourseDetail(page, teacherId);
	}
}
